package com.bix.util.blizfiles.m2;

import java.nio.ByteBuffer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.bix.util.blizfiles.BufferUtils;

/**
 * This class represents a standard animation block used in the Blizzard model
 * files. The class is abstract because there are different types of animation
 * blocks. Some are vectors, some are quaternions, etc. There are three methods
 * that subclasses must override to provide the necessary functionality.
 * 
 * @author squid
 * 
 * @version 1.0.0
 */
public abstract class M2AnimationBlock<T> {
	private static Log log = LogFactory.getLog (M2AnimationBlock.class);

	//
	// Animation type:
	// (0=none, 1=linear, 2=hermite)
	//
	enum AnimationType {
		NONE, LINEAR, HERMITE
	}

	private AnimationType type;

	//
	// If its made with a global sequence, its in here. Id is it is, -1 if not.
	//
	private short sequence;

	//
	// This is a count/offset pair to the list of timelines. There are 8 bytes
	// per timeline (yet another level of indirection). Each 8 bytes are another
	// count/offset pair pointing to the data for each timeline :(. This
	// information comes from this URL:
	// <p>
	// http://madx.dk/wowdev/wiki/index.php?title=Talk:M2/WotLK#On_animations
	//
	private M2CountOffsetPair timelines;
	private M2CountOffsetPair[] timelineData;
	private int[][] timelineTimes;

	//
	// This is a reference to the key frames of the animation. This information
	// is stored in the same manner as the time line data (see above). There is
	// a "third" part of the key frame data, namely, the actual values that
	// mirrors the timelineTimes member found above. For each entry in the
	// above array, there will be an entry in the key frame data values array
	// as well. The key frame data value member is up to the concrete classes
	// to provide.
	//
	private M2CountOffsetPair keyFrames;
	private M2CountOffsetPair[] keyFrameData;
	protected T[][] keyFrameDataValues;

	/**
	 * Instantiate an animation block from a ByteBuffer.
	 * 
	 * @param bb
	 */
	public M2AnimationBlock(ByteBuffer bb) {
		read (bb);
	}

	/**
	 * Read the animation block from a ByteBuffer.
	 * 
	 * @param bb
	 *          The buffer to read the animation from.
	 */
	public void read (ByteBuffer bb) {
		//
		// Get the type and convert it to an Enumeration.
		//
		switch (bb.getShort ()) {
		case 1:
			this.type = AnimationType.LINEAR;
			break;
		case 2:
			this.type = AnimationType.HERMITE;
			break;
		case 0:
		default:
			this.type = AnimationType.NONE;
			break;
		}
		
		this.sequence = bb.getShort ();
		this.timelines = new M2CountOffsetPair (bb);
		this.keyFrames = new M2CountOffsetPair (bb);

		log.debug ("type[" + this.type + "]");
		log.debug ("sequence[" + this.sequence + "]");
		log.debug ("timelines Count/Offset[" + this.timelines.getCount () + ",0x"
				+ Integer.toHexString (this.timelines.getOffset ()) + "]");

		//
		// Save the buffer position.
		//
		int position = bb.position ();

		//
		// Are there any timelines to read?
		//
		if (this.timelines.getCount () > 0) {
			this.timelineData = BufferUtils.getCountOffsetPairArray (bb, this.timelines);
			this.timelineTimes = new int[this.timelineData.length][];
			for (int i = 0; i < this.timelineData.length; i++) {
				log.debug ("  timeline #" + i + "[" + this.timelineData[i].getCount () + ",0x"
						+ Integer.toHexString (this.timelineData[i].getOffset ()) + "]");
				this.timelineTimes[i] = BufferUtils.getIntArray (bb, this.timelineData[i]);
				for (int j = 0; j < this.timelineTimes[i].length; j++) {
					log.debug ("    time #" + i + "/" + j + "[" + this.timelineTimes[i][j] + "]");
				}
			}
		}

		//
		// Are there any key frames to read?
		//
		if (this.keyFrames.getCount () > 0) {
			this.keyFrameData = BufferUtils.getCountOffsetPairArray (bb, this.keyFrames, false);
			log.debug ("keyFrames Count/Offset[" + this.keyFrames.getCount () + ",0x"
					+ Integer.toHexString (this.keyFrames.getOffset ()) + "]");
			getKeyFrameDataValues (bb);
		}

		//
		// Set the buffer's position back to where the data started.
		//
		bb.position (position);
	}

	/**
	 * This is a method that creates the new key frame data value array. Had to do
	 * this because Java doesn't allow you to use the 'new' operator on a generic
	 * type. Consider it a "factory-style" method inside of the generic class.
	 * 
	 * @return The newly created array.
	 */
	public abstract T[][] createKeyFrameDataValueArray ();

	/**
	 * This method reads a block of data values from a ByteBuffer. The location
	 * and size of the data is passed in the count/offset pair parameter.
	 * 
	 * @param bb
	 *          The ByteBuffer to read the data from.
	 * @param cop
	 *          The count/offset pair of where the data is located in the buffer
	 *          and how long the array is.
	 * 
	 * @return The block of data values read in.
	 */
	public abstract T[] readKeyFrameDataValues (ByteBuffer bb, M2CountOffsetPair cop);

	/**
	 * Interpolate two values.
	 * 
	 * @param p1
	 *          The first value to interpolate from
	 * @param p2
	 *          The second value to interpolate to
	 * @param r
	 *          The interpolation radius
	 * 
	 * @return The result of the interpolation.
	 */
	public abstract T interpolate (T p1, T p2, float r);

	/**
	 * This method reads the key frame data values from a ByteBuffer.
	 * 
	 * @param bb
	 *          The buffer to read the key frame data values from.
	 */
	public void getKeyFrameDataValues (ByteBuffer bb) {
		//
		// Allocate the enough space to hold each list of values and then
		// iterate over each list reading it from the buffer.
		//
		this.keyFrameDataValues = this.createKeyFrameDataValueArray ();
		for (int i = 0; i < this.getKeyFrameData ().length; i++) {
			//
			// Get the key frame data.
			//
			M2CountOffsetPair cop = this.getKeyFrameData ()[i];
			log.debug ("  keyFrame #" + i + "[" + cop.getCount () + ",0x" + Integer.toHexString (cop.getOffset ()) + "]");

			//
			// Read the list from the buffer.
			//
			this.keyFrameDataValues[i] = this.readKeyFrameDataValues (bb, cop);
			for (int j = 0; j < this.keyFrameDataValues[i].length; j++) {
				log.debug ("    value #" + i + "/" + j + "[" + this.keyFrameDataValues[i][j] + "]");
			}
		}
	}

	/**
	 * @return the type
	 */
	public AnimationType getType () {
		return type;
	}

	/**
	 * @return the sequence
	 */
	public short getSequence () {
		return sequence;
	}

	/**
	 * @return the times
	 */
	protected M2CountOffsetPair getTimelines () {
		return timelines;
	}

	/**
	 * @return The keyframes
	 */
	protected M2CountOffsetPair getKeyFrames () {
		return keyFrames;
	}

	/**
	 * @return the timelineData
	 */
	protected M2CountOffsetPair[] getTimelineData () {
		return timelineData;
	}

	/**
	 * @return the keyFrameData
	 */
	protected M2CountOffsetPair[] getKeyFrameData () {
		return keyFrameData;
	}

	/**
	 * Return the timeline data for all animation sequences in the block.
	 * 
	 * @return the timelineTimes
	 */
	public int[][] getTimelineTimes () {
		return timelineTimes;
	}

	/**
	 * Return the timeline data for the given animation sequence number.
	 * 
	 * @param animationSequence
	 *          The animation sequence number to get the timeline data for.
	 * 
	 * @return
	 */
	public int[] getTimelineTimes (int animationSequence) {
		if (this.timelineTimes == null || animationSequence < 0 || animationSequence >= this.timelineTimes.length) {
			return null;
		}
		return this.timelineTimes[animationSequence];
	}

	/**
	 * Returns the frame index that should be used for a given animation sequence
	 * and time. If the given time value is not valid, -1 is returned. Invalid
	 * times can be caused by timelines that have no data or where the time is
	 * longer than the actual length of the animation time.
	 * 
	 * @param animationSequence
	 *          Which animation sequence to use.
	 * @param time
	 *          The time
	 * 
	 * @return The frame index for the given time. If there is no timeline or the
	 *         given time goes beyond the length of the animation, than -1 is
	 *         returned.
	 */
	public int getFrameNumber (int animationSequence, int time) {
		//
		// Start advancing through the timeline data until we've reached the frame
		// that we're supposed to be on. The frame we're on is the first frame
		// who's time value is greater than the current time that we're searching
		// for.
		//
		int frame = 0;
		int[] times = this.getTimelineTimes (animationSequence);
		if (times == null) {
			return -1;
		}
		while (frame < times.length && times[frame] <= time) {
			frame++;
		}

		return (frame < times.length) ? frame - 1 : -1;
	}

	/**
	 * Returns the key frame data values for all animation sequences in the block.
	 * 
	 * @return The key frame data values.
	 */
	public T[][] getKeyFrameDataValues () {
		return keyFrameDataValues;
	}

	/**
	 * Return the key frame data values for the given animation sequence number.
	 * If the given animation sequences is invalid, null is returned.
	 * 
	 * @param animationSequence
	 *          The animation sequence number to get the data values for.
	 * 
	 * @return The animation key frame data values or null if the animation
	 *         sequences is invalid.
	 */
	public T[] getKeyFrameDataValues (int animationSequence) {
		if (keyFrameDataValues == null || animationSequence >= keyFrameDataValues.length) {
			return null;
		}

		return keyFrameDataValues[animationSequence];
	}

	/**
	 * Returns the data value used for a particular animation sequence and time.
	 * 
	 * @param animationSequence
	 * @param time
	 * 
	 * @return
	 */
	public T getKeyFrameDataValue (int animationSequence, int time) {
		//
		// Do we have any data to return, and, if so, is the animation sequence
		// valid?
		//
		T[][] values = this.getKeyFrameDataValues ();
		if (values == null || animationSequence < 0 || animationSequence >= values.length) {
			return null;
		}

		//
		// Get the frame number for this animation.
		//
		int frame = this.getFrameNumber (animationSequence, time);
		if (frame == -1) {
			return null;
		}

		//
		// Interpolate the data from the current frame to the next frame.
		//
		boolean interpolate = true;
		if (interpolate) {
			int t1 = this.getTimelineTimes ()[animationSequence][frame];
			int t2 = this.getTimelineTimes ()[animationSequence][frame + 1];
			if (t2 - t1 != 0) {
				float r = (float) (time - t1) / (float) (t2 - t1);

				if (this.getType () == AnimationType.LINEAR) {
					return this.interpolate (values[animationSequence][frame], values[animationSequence][frame + 1], r);
				}
			}
		}

		return keyFrameDataValues[animationSequence][frame];
	}
}
